-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(forge): revert diagnostic inspector #10446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(forge): revert diagnostic inspector #10446
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO on the right track, @klkvr pls share your thoughts re the approach.
left some minor comments / nits.
To note that such default option could add perf penalty for invariant tests running with fail_on_revert = false
but probably bearable.
outcome: CallOutcome, | ||
) -> CallOutcome { | ||
if outcome.result.result == InstructionResult::Revert { | ||
self.reverted = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe here we could directly set the reason? we can also do more checks if target address is actually a contract, (like checking if selector available, etc. some other similar hh checks https://github.com/NomicFoundation/hardhat/blob/67f1e95e1f3904f7b2e8a5560115c1551e899f64/packages/hardhat-core/src/internal/hardhat-network/stack-traces/solidity-errors.ts#L218-L341)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i thought that storing a bool could be more efficient + fn reason()
would allow us to isolate the logic, but totally fine for me to do it directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need some smarter strategy here to ensure that call we've caught in call
hook is indeed the one that caused revert (e.g compare depth, ensure there were no calls after it)
crates/forge/tests/cli/test_cmd.rs
Outdated
interface ICounter { | ||
function number() external returns (uint256); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that in cases of calling functions returning data Solidity's approach is to revert if data is empty.
However, if method does not return anything (e.g like increment()
) then Solidity would firstly check before the call whether the address has any code, and revert if it's not which I believe is not being caught by our inspector rn
addresses the PR feedback by checking the call stack depth + tracking EXTCODESIZE calls to empty addresses (confirmed that the initial impl wouldn't diagnose those). the only downside is extra performance overhead, but i tried to make the |
i also enhanced the trace decoder to identify function calls for a selector that is not supported by the called contract (if the tracer has access to its abi) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm! one comment re enabling diagnostics and tracing, pls check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense, thank you! pending others review before merging
CC @klkvr
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very nice! overall the approach lgtm, this feature is kind of tricky because we're basically forced to do bytecode/execution pattern matching and I'd like us to reduce false positives here and make sure we document this clearly because this logic will likely get more complex over time
crates/evm/traces/src/decoder/mod.rs
Outdated
// Check if unsupported fn selector: calldata dooes NOT point to one of its selectors + | ||
// non-fallback contract + no receive | ||
if let Some(contract_selectors) = self.non_fallback_contracts.get(&trace.address) { | ||
if !contract_selectors.contains(&selector) && | ||
(!cdata.is_empty() || !self.receive_contracts.contains(&trace.address)) | ||
{ | ||
let return_data = if !trace.success { | ||
let revert_msg = | ||
self.revert_decoder.decode(&trace.output, Some(trace.status), None); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will only work for local contracts right? I'm wondering if there's some common bytecode pattern that is being reached when contract is called with such wrong calldata which we could catch and make this logic work for any contract for which we don't know abi
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, exactly this would only provide insights if we know the abi beforehand
i guess a generic approach would be to perform some sort bytecode analysis and identify when the PC doesn't do any jumps on the function dispatcher and ends up reverting (idk if this vyper uses the same approach as solidity though)
@klkvr i've addressed all the feedback that u left:
lmk if i should fix something else 🙂 PS: as discussed, we'd leave the trace decoder improvement (where we would analyze that there are no matches in the fn dispatcher) for another PR |
…ert-diagnostic-inspector
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, only have small nits
if let Ok(state) = ctx.journal().code(target) { | ||
if state.is_empty() && !inputs.input.is_empty() { | ||
self.non_contract_call = Some((target, inputs.scheme, ctx.journal().depth())); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: pattern CALL -> REVERT
actually means that Solidity was not able to decode outputs which might happen even when contract has code but reported with invalid abi encoding like in e.g common case of ERC20 transfer not returning bool for some tokens. Ideally we should also be able to catch this case but we can track this separately
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'll open a new issue for that one and the unsupported selector.
just to be certain that i don't miss anything:
- missmatch between bytecode and assumed abi: we can catch it cause there is a successful call, followed by a revert at the same depth where the call took place --> to avoid false positives, we should know check the returndata validation logic, but that requires careful inspection of the opcodes following the call. any hints on what u'd do exactly?
- call to non supported fn selector: we can catch it (at least assuming that the target is a solidity/vyper contract) if, when a call takes places, there is a revert without any previous positive
JUMPI
, as that would mean that the PC walks through the whole fn dispatcher without finding a match --> using the traces decoder is limited to local contracts, but will never throw false positives. with this logic we could do everything with the inspector alone, which i like. however, do you think the approach is sound enough?
@grandizzy finally ready, with @klkvr last round of feedback incorporated! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you, good to send it from my pov! We should make some comparison with the project we use to have as a baseline (see #10183 (comment)) to be aware of penalty it introduce when forge test with traces.
@klkvr @zerosnacks @DaniPopes last pass through before merge please, thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm!
closes
design
Created a new
RevertDiagnostic
inspector that provides detailed information of otherwise undefined evm reverts. For the time being, the inspector suports:Additionally, enhanced the traces decoder to determine when a function call to an unkown selector takes places (only for local contracts with artifacts).
output
these would be the new error messages that users would see:
benchmarks
forge test -vvvv
notes:
conclusion: the new inspector makes tests (on average) ~11% slower... the bright side is that it would only impact those test runs where traces are active (which i would assume to only be when actively debugging).
do we feel like this is acceptable?